spb/autoscholar
Public
1import { getInstructionVersion, getProject, listRuns } from "@autoscholar/core";2import Link from "next/link";3import { notFound } from "next/navigation";4import { CodeBlock } from "@/components/CodeBlock";5import { MarkdownView } from "@/components/MarkdownView";6import { StatusBadge } from "@/components/StatusBadge";7import { disciplineLabel, fmtDateTime, fmtUsd } from "@/lib/format";8import { db } from "@/lib/server/db";910export const dynamic = "force-dynamic";1112export default async function ProjectDetail({13 params,14 searchParams,15}: {16 params: Promise<{ id: string }>;17 searchParams: Promise<{ v?: string }>;18}) {19 const { id } = await params;20 const { v } = await searchParams;21 const d = db();22 let data: Awaited<ReturnType<typeof getProject>>;23 try {24 data = await getProject(d, id);25 } catch {26 notFound();27 }28 const runs = await listRuns(d, { projectId: id, limit: 50 });29 const selected = v30 ? await getInstructionVersion(d, v).catch(() => null)31 : data.versions[0]32 ? await getInstructionVersion(d, data.versions[0].id)33 : null;34 return (35 <div className="space-y-6">36 <header>37 <p className="eyebrow">38 <Link href="/admin/projects">Projects</Link>39 </p>40 <h1 className="title-2 mt-1">{data.project.title}</h1>41 <p className="muted text-sm">42 {disciplineLabel(data.project.discipline)} · {data.project.description || "no description"}43 </p>44 </header>45 <div className="grid gap-6 lg:grid-cols-12">46 <section className="surface p-4 lg:col-span-4">47 <h2 className="title-3">Instruction versions</h2>48 <ul className="mt-2 space-y-2 text-sm">49 {data.versions.length === 0 && <li className="muted">None uploaded.</li>}50 {data.versions.map((iv) => (51 <li52 key={iv.id}53 className={`rounded-md border p-2 hairline ${selected?.id === iv.id ? "bg-[var(--bg-muted)]" : ""}`}54 >55 <Link href={`/admin/projects/${id}?v=${iv.id}`}>56 v{iv.version} · {iv.filename}57 </Link>58 <p className="subtle font-mono text-xs">59 {iv.sha256.slice(0, 16)} · {iv.byteSize} B · {fmtDateTime(iv.createdAt)}60 </p>61 {iv.note && <p className="muted text-xs">{iv.note}</p>}62 <Link href={`/admin/new?project=${id}`} className="btn btn-sm mt-2">63 Re-run / duplicate64 </Link>65 </li>66 ))}67 </ul>68 <h2 className="serif mt-6 text-lg font-semibold">Runs</h2>69 <ul className="mt-2 space-y-1 text-sm">70 {runs.length === 0 && <li className="muted">No runs.</li>}71 {runs.map((r) => (72 <li key={r.id} className="flex items-center justify-between gap-2">73 <Link href={`/admin/runs/${r.id}`}>{r.title}</Link>74 <span className="flex items-center gap-2">75 <StatusBadge status={r.status} />76 <span className="subtle text-xs">{fmtUsd(r.totalCostUsd)}</span>77 </span>78 </li>79 ))}80 </ul>81 </section>82 <section className="lg:col-span-8">83 {selected ? (84 <div className="space-y-3">85 <div className="flex flex-wrap items-center justify-between gap-2">86 <h2 className="title-3">Instruction v{selected.version} (immutable)</h2>87 <span className="font-mono text-xs break-all">{selected.sha256}</span>88 </div>89 {selected.frontmatter && (90 <CodeBlock91 code={JSON.stringify(selected.frontmatter, null, 2)}92 lang="json"93 maxHeight="10rem"94 />95 )}96 <details open className="surface p-4">97 <summary className="cursor-pointer text-sm">Rendered preview (sanitized)</summary>98 <div className="mt-3">99 <MarkdownView markdown={selected.contentText} />100 </div>101 </details>102 <details className="surface p-4">103 <summary className="cursor-pointer text-sm">Raw text</summary>104 <CodeBlock code={selected.contentText} lang="md" />105 </details>106 </div>107 ) : (108 <p className="muted">Select a version.</p>109 )}110 </section>111 </div>112 </div>113 );114}115